/**
 * Copyright (C) 2011-2012 Luca Li Greci
 *
 * This file is part of pojo-creator
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.pojocreator.utility;

import java.lang.reflect.Field;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * This class invokes the right setter based on the datatype of source and
 * destination.
 * 
 * @author Luca Li Greci
 */
final class SetterInvoker {
    private Map < String, Setter > setterFunctor;

    /**
     * Create in instance of SetterInvoker.
     */
    SetterInvoker() {
        setterFunctor = new HashMap < String, Setter >();
        setterFunctor.put("List", new ListSetter());
        setterFunctor.put("byte", new PrimitiveByteSetter());
        setterFunctor.put("Long", new LongSetter());
        setterFunctor.put("DefaultSetter", new DefaultSetter());
    }

    /**
     * Dispatch the method invocation to the correct {@link Setter} functor.
     * 
     * @param field Field to set.
     * @param object Object to set.
     * @param objectValue Value to set.
     * @throws IllegalAccessException in case of error.
     */
    void invoke(Field field, Object object, Object objectValue) throws IllegalAccessException {
        Setter setter = findBy(field.getType());
        setter.set(field, object, objectValue);
    }

    /**
     * Access the defined map of setters and return the right one based on the
     * on the destination type
     * 
     * @param destination class type to set
     * @return the Setter object which will set the field.
     */
    private Setter findBy(final Class < ? > destination) {
        String destinationType = destination.getSimpleName();
        Setter setter = setterFunctor.get(destinationType);
        if (setter == null) {
            setter = setterFunctor.get("DefaultSetter");
        }
        return setter;
    }
    
    /**
     * Functor to set a primitive byte.
     */
    private static class LongSetter implements Setter {
        
        @Override
        public void set(final Field field, final Object object, final Object objectValue) throws IllegalAccessException {
            AccessController.doPrivileged(new AccessibleField(field));
            Long value = null;
            if (objectValue instanceof Integer) {
                value = Long.valueOf(((Integer) objectValue).longValue());
            } else {
                value = (Long)objectValue;
            }
            field.set(object, value);
            
        }
    }

    /**
     * Functor to set a primitive byte.
     */
    private static class PrimitiveByteSetter implements Setter {
        
        @Override
        public void set(final Field field, final Object object, final Object objectValue) throws IllegalAccessException {
            AccessController.doPrivileged(new AccessibleField(field));
            field.setByte(object, ((Integer) objectValue).byteValue());
        }
    }

    /**
     * Functor to set a {@link List} of object.
     */
    private static class ListSetter implements Setter {
        /**
         * The objectValue passed in can be:
         * <ul>
         * <li>a single object to add in the list</li>
         * <li>an implementation of list (e.g {@link ArrayList}) which contains
         * the POJO to be added.</li>
         * </ul>
         */
        @Override
        public void set(Field field, Object object, Object objectValue) throws IllegalAccessException {
            AccessController.doPrivileged(new AccessibleField(field));
            field.set(object, buildPopulatedList(field, object, objectValue));
        }

        /**
         * Create a populated {@link List} with the correct data type. The
         * returned list contains all elements specified in objectValue. If the
         * object value represents a single element and the current list is
         * empty, the element will be added.
         * 
         * 
         * @param field Field that defines the list
         * @param object Object owner of the list
         * @param value Property's value to add into the list.
         * @return a {@link List} with provided objectValues
         * @throws IllegalAccessException
         */
        @SuppressWarnings("unchecked")
        private < E > List < E > buildPopulatedList(Field field, Object object, E value) throws IllegalAccessException {
            List < E > list = (List < E >) field.get(object);
            if (list == null && valueImplementsList(field, value)) {
                return (List < E >) value;
            }
            if (list == null) {
                list = new ArrayList < E >();
            }
            checkAndCollectElement(field, value, list);
            return list;
        }

        /**
         * Check if the value type implements {@link List} interface. For
         * instance in case the value is already an {@link ArrayList}.
         * 
         * @param field Field to set.
         * @param value Value to set. This can be already a list
         * @return <code>true</code> when the value implements {@link List},
         *         <code>false</code> otherwise.
         */
        private < E > boolean valueImplementsList(final Field field, final E value) {
            return field.getType().isAssignableFrom(value.getClass());
        }

        /**
         * Check the value can be added in the collector list, if the type are
         * compatible the element is added in the collector otherwise and
         * {@link IllegalArgumentException} describes the type passed in and the
         * type the list can accept.
         * 
         * @param field Field to set.
         * @param value Value to set.
         * @param collector Collection of element which contains the element.
         * @throws IllegalArgumentException in case of incompatible type.
         */
        private static < E > void checkAndCollectElement(final Field field, final E value, final List < E > collector)  {
            Class < ? > elementType = ReflectionHelper.collectionOf(field);
            if (elementType != null && elementType.isAssignableFrom(value.getClass())) {
                collector.add(value);
            } else {
                throw new IllegalArgumentException("List of " + elementType + " cannot accept type of " + value.getClass());
            }
        }
    }

    /**
     * Functor for a generic setter.
     */
    private static class DefaultSetter implements Setter {
        @Override
        public void set(Field field, Object object, Object objectValue) throws IllegalAccessException {
            AccessController.doPrivileged(new AccessibleField(field));
            field.set(object, objectValue);
        }
    }

    
    /**
     * Interface that defines the set method to be invoked.
     */
    private interface Setter {
        /**
         * Set the specified {@link Field} in the Object with the correct value
         * 
         * @param field Field to set.
         * @param object Object to set.
         * @param value Value to set.
         * @throws IllegalArgumentException in case of error.
         * @throws IllegalAccessException in case of error.
         */
        void set(Field field, Object object, Object value) throws IllegalAccessException;
    }


    /**
     * Set the accessible flag in a PrivilagedAction 
     */
    private static final class AccessibleField implements PrivilegedAction < Void > {
        private final Field field;

        private AccessibleField(Field field) {
            this.field = field;
        }

        @Override
        public Void run() {
            field.setAccessible(true);
            return null;
        }
    }
}
